# --------------------------------------------------------------------------------
#  NoiseTube Web application
#  
#  Copyright (C) 2008-2010 SONY Computer Science Laboratory Paris
#  Portions contributed by Vrije Universiteit Brussel (BrusSense team), 2008-2011
# --------------------------------------------------------------------------------
#  This library is free software; you can redistribute it and/or modify it under
#  the terms of the GNU Lesser General Public License, version 2.1, as published
#  by the Free Software Foundation.
#  
#  This library is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#  FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
#  details.
#  
#  You should have received a copy of the GNU Lesser General Public License along
#  with this library; if not, write to:
#    Free Software Foundation, Inc.,
#    51 Franklin Street, Fifth Floor,
#    Boston, MA  02110-1301, USA.
#  
#  Full GNU LGPL v2.1 text: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt
#  NoiseTube project source code repository: http://code.google.com/p/noisetube
# --------------------------------------------------------------------------------
#  More information:
#   - NoiseTube project website: http://www.noisetube.net
#   - Sony Computer Science Laboratory Paris: http://csl.sony.fr
#   - VUB BrusSense team: http://www.brussense.be
# --------------------------------------------------------------------------------
 
require 'time'
require 'sound_level.rb'
require 'rgeo'

class Measure < ActiveRecord::Base
  @geographic_factory = RGeo::Geographic.spherical_factory(:srid => 4326)
  # Support Cartesian (flat) geometry
  #geographic_factory = RGeo::Geographic.spherical_factory(:srid => 4326)

  #acts_as_geom :geom  => :point
  #acts_as_taggable_on :tags, :autoclienttags


  belongs_to :track
  belongs_to :user

  validates_numericality_of :loudness
  validates_presence_of :user
  validates_presence_of :track

  attr_accessible :user, :track, :made_at, :loudness, :loudness_index, :geom #:ozone, :temperature, :humidity, :pressure, :wind



  # ######################## DAO FUNCTIONS ######################

  def self.search(params, includes=nil, is_admin = false)
    cond, joins = Measure.search_options(params)
    if cond.nil?
    	raise SearchException.new("Result too large, please add parameters to limit search space")
    else
      q = Measure.includes(includes).joins(joins).order(" measures.made_at DESC").where(cond)
      q = q.limit(API_LIMIT) unless ( is_admin)
	#return Measure.all(:include=>includes, :joins=>joins, :conditions=>cond, :limit=>500, :order => "made_at DESC")
      q.all
    end

  end

  def isvalid?
    SoundLevel.valid?(self.loudness)
  end
  
  # generic search query preparation
  def self.search_options(params)  
    cond=[]
    joins=[]

    # LOUDNESS
    cond<<"loudness <#{params[:dbmax]}" unless params[:dbmax].blank?
    cond<<"loudness >#{params[:dbmin]}" unless params[:dbmin].blank?

    # TAG
    # TODO: does not seem to work, FIX THIS
    unless params[:tags].blank?
      joins<<"INNER JOIN measurements_tags ON measurements_tags.begin_id = measures.id" #todo: modify for intervals
      #joins<<"INNER JOIN tags ON tags.id = taggings.tag_id"
      cond1=[]
      params[:tags].split(",").each{ |tag|
        # from older versions of noisetube, tag search was like
        # tags=context:tagname
        # this version changes: search tags in this way:
        # tags=tagname:context or just tags=tagname
        name,context=tag.split(":")
        # context="tags" if context.nil?

        # add tagname and/or context to the query
        unless name.nil?
          name=name.gsub(/\\/, '\&\&').gsub(/'/, "''")
          namecond = " measurements_tags.tag_name =E'#{name}' "
        end
        unless context.nil?
          contextcond = " measurements_tags.context=E'#{context}' "
        end

        if !name.nil? and !context.nil?
          cond1 << " ( " << namecond << " and " << contextcond << " ) "
        else
        cond1 << namecond unless namecond.blank?
        cond1 << contextcond unless contextcond.blank?

        end
      }
      cond<< "measures.id IN (SELECT begin_id FROM measurements_tags WHERE #{cond1.join(" OR ")} )"   #TODO: perfomance improvement using ANY!!
    end

    # Geoprahical bounding box #
    unless params[:geo].blank?
      cond<< Measure.geoboxCond(params[:geo])
    else
      unless params[:box].blank?
        cond<< Measure.geoboxCond(params[:box])
      end
    end	
	
    # CITY
    #TODO city by name
    city=City.find(params[:city]) unless params[:city].blank? 	# by ID
    unless city.nil?
      cond<<"tracks.city_id=#{city.id}"
      joins<<"LEFT OUTER JOIN tracks ON tracks.id = measures.track_id"
    end

    # TRACK
    track=Track.find(params[:track]) unless params[:track].blank?
    unless track.nil?
      cond<<"tracks.id=#{track.id}"
      joins<<"LEFT OUTER JOIN tracks ON tracks.id = measures.track_id "
    end

    # USER
    unless params[:user].blank?
         u = nil
    	 begin
    	   u = User.find(params[:user])				# by ID
    	 rescue Exception
	   #ignore
    	 end
    	 begin
    	   u = User.find_by_login(params[:user]) if u.nil?	# by login
    	 rescue Exception
	   #ignore
    	 end
    	 cond<<" measures.user_id=#{u.id}" unless u.nil?
    end
    
    # TIME
    #TODO since
    #TODO until
    begin
      unless params[:since].blank?
        since_time = DateTime.strptime(params[:since], W3C_DATE_TIME_FORMAT)
        cond << " measures.made_at > '#{since_time.to_s(:db)}'"
      end
      unless params[:until].blank?
        until_time = DateTime.strptime(params[:until], W3C_DATE_TIME_FORMAT)
        cond << " measures.made_at '#{until_time.to_s(:db)}'"
      end
    rescue Exception=> ex
       raise SearchException.new 'Invalid date format of since/until. Accepted dates should be in yyyy-mm-dd format.'
    end
    # MAX NUMBER
    #TODO max


    [cond.size>0? cond.join(" and ") : nil, joins.join(" ")]
    
  end

  def tags
    get_tags()
  end

  # depre6  save the noise exposure with its meta-data
  #@param params - the params from the request
  #@param track - the running track
  #@param user - the user performing the update
  def self.save_request(params, track, user)

    # time
    made_at = Time.now
    made_at = DateTime.parse(params[:time]) unless params[:time].blank?
    tag_args = {:with => params[:tag], :on => :location}

    # store the measure
    measure=Measure.new(:made_at=>made_at,
                        :loudness=> params[:db],
                        :user=>user,
                        :track=>track)

    #loudness_index
    measure.loudness_index = SoundLevel.compute_loudness_index(measure.loudness)
    
    # location
    loc = params[:l]
    # if location is lat,lng coordinates
    if loc=~ /geo\:(.*),(.*)/
      measure.geom=@geographic_factory.point($2.to_f, $1.to_f)
      #measure.geom.srid = 4326 #WGS84
    else
    # if location is tag location
      tag_args = {:with => loc.downcase, :on => "location"} unless loc.blank?
    end



    #TAGGING
    #automatic tags passed as normal tags by older NTMobile versions
    if params[:tag]=="variation:high"
      measure.peak=1
    elsif params[:tag]=="exposure:high"
      measure.longexposure=1      
    else
      unless params[:tag].blank?
    #human tags
    #params[:tag].split(/ |,|;/).each { |t|  #split at spaces, commas and semicolons (tagging framework only splits at spaces)
    #  logger.error t

      tag_args = {:with=>params[:tag], :on=>"user"}
    #}
      end
    end

    #new way of passing automatic tags from NTMobile client: all tags take form "tagger:tag", they are space-separated (e.g.: "tagger1:tagA tagger2:tagB")
    unless params[:autotag].blank?
      tag_args = { :with=>params[:autotag], :on=>"auto"}
    end

    #new - tag intervals . todo:
    unless params[:id].blank?
      #measure.mclientid = params[:id]  unless params[:id].nil?
      #measure.msessionid = params[:sessionid] unless params[:sessionid].nil?
    end

    #save
    measure.save

    logger.info "Saved measurement. ID:" + measure.id.to_s + " track: " + measure.track.id.to_s + " by user: " + measure.user.id.to_s

    measure_tag = MeasurementsTag.new(
        tag_name: tag_args[:with],
        track: track,
        begin_id: measure.id,
        end_id: measure.id,
        context: tag_args[:on],
        user: user
    )

    #save the tag. todo: how to handle tag intervals in api
    measure_tag.save!
  end

  # LOUDNESS INDEXATION
  def self.by_range(measures)
    ranges=[]
    SoundLevel::STEP_DB.times {|i|
      measures_in_range=measures.find(:all, :conditions => "loudness_index=#{i} and measures.geom is not null")
      ranges<<{:label=>SoundLevel::DISTRIBUTION_LABELS[i], :measures=>measures_in_range}
    }
    return ranges
  end

  # Measure.by_range_json
  # Loudness Indexation - Without Active Objects
  def self.by_range_json(measures)
    ranges=[]
    SoundLevel::STEP_DB.times {|i|
      measures_in_range=measures.select{ |m|
        m["loudness_index"] == i and !m["lat"].nil? }
      ranges<<{:label=>SoundLevel::DISTRIBUTION_LABELS[i], :measures=>measures_in_range}
    }
    return ranges
  end

  # LOCAL TIME INDEXATION
  def self.compute_time_index(measure)
    case measure.created_at.localtime.hour #WHY? localtime?
      when 0..5 then
        return 0
      when 6..11 then
        return 1
      when 12..17 then
        return 2
      when 18..21 then
        return 3
      when 22..23 then
        return  0
    end
  end  

  # function wkt_from_geo
  def self.geo_from_loc(loc)
    # if location is lat,lng coordinates
    if loc=~ /geo\:(.*),(.*)/
      geom=@geographic_factory.point($2.to_f, $1.to_f)
      #measure.geom.srid = 4326 #WGS84
    else
      geom=@geographic_factory.point($2.to_f, $1.to_f)  if loc=~ /(.*),(.*)/
    end
    geom
  end
  # function wkt_from_geo
  def self.wkt_from_loc(loc)
    g = Measure.geo_from_loc(loc)
    g.nil? ? g = "" : g.as_text
    g
  end

  # function save_measurements_as_transactions - saves measures as batch
  #@param query - the starting query
  #@param values - the values array
  def self.save_as_transactions(query, values)
    ids = []
    sql = query << values.join(",") << " RETURNING id"
    self.transaction do
      result = ActiveRecord::Base.connection.execute sql
      #return ids as array
      result.each do |row|
        ids << row["id"] unless row["id"].nil?
      end
    end
    ids
  end

  # ---------
  def tag_list(track_tags=nil)
    #easier to get all track tags
    track_tags = MeasurementsTag.where("track_id = #{self.track_id}").all if track_tags.nil?
    my_tags = []
    track_tags.each{|tag|
      if (self.id == tag.begin_id and tag.end_id == -1) or (self.id.between?(tag.begin_id, tag.end_id) )
        my_tags << tag
      end

    } unless track_tags.blank?
    unless my_tags.blank?
      ret = my_tags.map(&:tag_name).join(", ")
    else
      ret = []
    end
    ret
  end

  # ------------ for json serialization ----------------
  def as_json(options = { })
    m = super(options.reverse_merge(:only => [:id, :created_at, :loudness, :made_at, :track_id, :user_id, :loudness_index]))
    m[:lat] = self.geom.try(:lat)
    m[:lng] = self.geom.try(:lng)
    #m[:point] = self.geom.try(:as_text)
    m
  end

  # --------------------- private area ----------------------------
  private
  
  # Geographical bounding box #
  def self.geoboxCond(paramvalue)
      box=paramvalue.split(",")
      if box.size==4
        # condition
        box_left=box[0]
        box_bottom=box[1]
        box_right=box[2]
        box_top=box[3]
        #return "measures.lat<#{box_top} and measures.lat>#{box_bottom} and measures.lng<#{box_right} and measures.lng>#{box_left}"
        return " measures.geom && ST_MakeEnvelope(#{box_left}, #{box_bottom}, #{box_right}, #{box_top}, 4326) "
      else
        raise SearchException.new("parameter box #{paramvalue} not valid")
      end    
  end


  # ---------------- NEW TAGGING FUNCTIONALITY --------------------- #
  def self.tagged_with(name)
    MeasurementsTag.find_by_tag_name!(name)
  end

  def tag_list=(names)
    tags = names.split(",").map do |n|
      MeasurementsTag.where(name: n.strip).first_or_create!
    end
  end
  def get_tags()
    MeasurementsTag.tags_for_measure(self.id)
  end
  # ---------------------------------------------------------------- #


end
